-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: SwiftUI View Rendering Instrumentation #20
Conversation
844bdc7
to
7f08ddf
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some early feedback.
…-swift into mh/instrumented-view
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, this looks great, and I just have a bunch of nits. But also, are you going to add some SmokeTest UI tests and assertions?
|
||
ViewInstrumentationView() | ||
.padding() | ||
.tabItem { Label("View Instrumentation", systemImage: "ruler") } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's really hard to resist the urge to bikeshed what icon to use for each tab 😆
} onEditingChanged: { editing in | ||
isEditing = editing | ||
if !editing { | ||
delay = sliderDelay |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really thought that since the value of the slider was a $
binding that you would just be able to have Slider(value: $delay,
and not have any onEditingChanged:
handler. If this is necessary, could you add a comment explaining why we need all three state variables?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah the idea was that we'd be able to adjust the slider but the rest of the view wouldn't re-render until you stopped editing it.
BUT clearly I wasn't paying enough attention and that wasn't actually working. I'll restructure it and add a comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
works now:
Screen.Recording.2024-11-15.at.2.48.07.PM.mp4
(yes i know the simulator has a screen recorder but i wanted to capture my mouse cursor lol)
|
||
private func timeConsumingCalculation(_ delay: Double) -> String { | ||
print("starting time consuming calculation") | ||
sleep(UInt32(delay)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sleep()
is the POSIX function that takes a whole number of seconds, but it would be better to use the Swift function that takes a TimeInterval
: Thread.sleep(forTimeInterval: delay)
} | ||
} | ||
|
||
private func timeConsumingCalculation(_ delay: Double) -> String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than Double
, it would be more idiomatic and self-documenting to use TimeInterval
, which is an alias for Double
. I would try to use it as much as possible. Like, if we still need two state variables above, probably at least the delay
one should be TimeInterval
too.
} | ||
|
||
var body: some View { | ||
print("\(name) body started rendering") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't forget to remove print
statements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have any kind of debug logging infrastructure? I've found these logs to be helpful when I play with the smoke tests.
|
||
print("\(name) content appeared") | ||
span.setAttribute(key: "TotalDuration", value: Date().timeIntervalSince(initTime)) | ||
span.end(time: Date()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be better to capture the Date()
once and use the same value on both these lines?
print("\(name) content appeared") | ||
span.setAttribute(key: "TotalDuration", value: Date().timeIntervalSince(initTime)) | ||
span.end(time: Date()) | ||
bodySpan.end(time: endTime) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little confused about this. Is there a reason we can't do this on line 40, right after endTime
is set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted it to remain active in context, I put some comments in the code that should it explain it better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for the comment. very helpful.
@@ -20,7 +20,7 @@ span_attributes_for() { | |||
# $3 - attribute key | |||
# $4 - attribute type | |||
attribute_for_span_key() { | |||
attributes_from_span_named $1 $2 | \ | |||
attributes_from_span_named "$1" "$2" | \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're emitting spans named View Render
so these args need to be quoted for our utils to work correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great. I made a few comments for future discussion, but they shouldn't prevent us from submitting this.
README.md
Outdated
`View Render` spans encompass the entire rendering process, from initialization to appearing on screen. They include the following attributes: | ||
- `ViewName` (string): the name passed to `HoneycombInstrumentedView` | ||
- `RenderDuration` (double): amount of time to spent initializing the contents of `HoneycombInstrumentedView` | ||
- `TotalDuration` (double): amount of time from when `HoneycombInstrumentedView::body()` is called to when the contents appear on screen |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: it's .
, not ::
. This isn't C++. :-)
struct DelayedSlider: View { | ||
@State private var sliderDelay = 2.0 | ||
@State private var isEditing = false | ||
var delay: Binding<Double> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love it! <3
|
||
Specifically, it will emit 2 kinds of span for each view that is wrapped: | ||
|
||
`View Render` spans encompass the entire rendering process, from initialization to appearing on screen. They include the following attributes: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm surprised there aren't specific semantic conventions around spaces and capitalization in span names, but it appears there are not: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.37.0/specification/trace/api.md#span
Anyway, this is fine for now, and we can always tweak them later, since we're still in experimental alpha mode. We should probably just be consistent with our web sdk.
print("\(name) content appeared") | ||
span.setAttribute(key: "TotalDuration", value: Date().timeIntervalSince(initTime)) | ||
span.end(time: Date()) | ||
bodySpan.end(time: endTime) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for the comment. very helpful.
@test "Render Instrumentation attributes are correct" { | ||
# we got the spans we expect | ||
result=$(span_names_for "@honeycombio/instrumentation-view" | sort | uniq -c) | ||
assert_equal "$result" ' 7 "View Body" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should keep an eye on this and make sure it isn't flaky. I'm a little worried about something getting rendered an extra time for whatever reason, and the counts being off.
Which problem is this PR solving?
Short description of the changes
SwiftUI rendering is very interesting!
This PR currently has a whole bunch of instrumentation and logging which makes it easier to see what's going on
This PR intentionally does not ship a view modifier form of the instrumentation. This is because ViewModifiers receive a
Content
struct (vs. Views that receive an@ViewBuilder () -> Content
callback!); that is: ViewModifiers receive a view after it has been initialized, and therefore have no way to retroactively start a span before the view begins initializing.Compare the spans/log output from using the wrapper view form:
with the view modifier form:
And, now that we've settled on the wrapper form, here's the kinds of traces we're producing:
How to verify that this has the expected result